#!/usr/bin/env python

#
# This file is part of pySMPP
# Copyright (C) 2003 Damjan Georgievski
# 		2011 Gustaw Lewandowski 
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA

import struct
class PDUError(Exception):
	def __init__(self, value):
		self.value = value
	def __str__(self):
		return self.value

SMPP_SUBMIT_SM		= 0x00000004
SMPP_SUBMIT_SM_RESP	= 0x80000004
SMPP_DATA_SM		= 0x00000103
SMPP_DATA_SM_RESP	= 0x80000103
SMPP_DELIVER_SM		= 0x00000005
SMPP_DELIVER_SM_RESP	= 0x80000005
SMPP_BIND_RX		= 0x00000001
SMPP_BIND_RX_RESP	= 0x80000001
SMPP_BIND_TX		= 0x00000002
SMPP_BIND_TX_RESP	= 0x80000002
SMPP_BIND_TRX		= 0x00000009
SMPP_BIND_TRX_RESP	= 0x80000009
SMPP_UNBIND		= 0x00000006
SMPP_UNBIND_RESP	= 0x80000006
SMPP_ENQUIRE_LINK       = 0x00000015
SMPP_ENQUIRE_LINK_RESP  = 0x80000015

def SMPP_cmd_name(cmd):
	""" convert command_id to str """
	if cmd == SMPP_SUBMIT_SM:
		return "SUBMIT_SM"
	if cmd == SMPP_SUBMIT_SM_RESP:
		return "SUBMIT_SM_RESP"
	if cmd == SMPP_DATA_SM:
		return "DATA_SM"
	if cmd == SMPP_DATA_SM_RESP:
		return "DATA_SM_RESP"
	if cmd == SMPP_DELIVER_SM:
		return "DELIVER_SM"
	if cmd == SMPP_DELIVER_SM_RESP:
		return "DELIVER_SM_RESP"
	if cmd in [SMPP_BIND_RX, SMPP_BIND_TX, SMPP_BIND_TRX]:
		return "BIND_SM"
	if cmd in [SMPP_BIND_RX_RESP, SMPP_BIND_TX_RESP, SMPP_BIND_TRX_RESP]:
		return "BIND_SM_RESP"
	if cmd == SMPP_UNBIND:
		return "SMPP_UNBIND"
	if cmd == SMPP_UNBIND_RESP:
		return "SMPP_UNBIND_RESP"
	if cmd == SMPP_ENQUIRE_LINK:
		return "SMPP_ENQUIRE_LINK"
	if cmd == SMPP_ENQUIRE_LINK_RESP:
		return "SMPP_ENQUIRE_LINK_RESP"
	return "Unknown"

class PDU():
	obj_type = None
	_look = None
	_changed = True
	def __init__(self, data = None):
		if data == None:
			self.sequence_number = 0
			self.command_status = 0
			self.command_length = 0
			self.body = None
		else:
			self.decompile(data)

	def __len__(self):
		return self.command_length + 16

	def decompile(self, s):
		l = len(s)
		if l < 16:
			raise PDUError("Illegal PDU (length too short)")

		#parse header
		( self.command_length,
		  self.command_id,
		  self.command_status,
		  self.sequence_number ) = struct.unpack('>4I',s[0:16])

		if l == 16:
			# message without body or only header of message
			self.body = None
			return None

		# l is length of the msg body
		l -= 16
		self.body = struct.unpack(`l`+'s',s[16:])[0]

		if self.command_id == SMPP_SUBMIT_SM or self.command_id == SMPP_DELIVER_SM:
			self.service_type, rest = self.body.split('\x00', 1)
			(self.source_addr_ton, self.source_addr_npi) = struct.unpack('>BB', rest[:2])
			self.source_addr, rest = rest[2:].split('\x00', 1)
			(self.dest_addr_ton, self.dest_addr_npi) = struct.unpack('>BB', rest[:2])
			self.dest_addr, rest = rest[2:].split('\x00', 1)
			(self.esm_class, self.protocol_id, self.priority_flag) = struct.unpack('>BBB', rest[:3])
			self.schedule_delivery_time, self.validity_period, rest  = rest[3:].split('\x00', 2)
			(self.registered_delivery, self.replace_if_present_flag,
			self.data_coding, self.sm_default_msg_id, self.sm_length) = struct.unpack('>BBBBB', rest[:5])
			self.short_message = rest[5:5 + self.sm_length]
			if self.esm_class & 64 == 64:
				self._udh_len = struct.unpack('>B', self.short_message[:1])[0]
				self._udh_len += 1
				self._udh = self.short_message[:self._udh_len]
				self.short_message = self.short_message[self._udh_len:]
			self.obj_type = 'SMPP_SUBMIT_SM'
			# optional parameters
			rest = rest[5 + self.sm_length:]
			while len(rest) > 4:
				(t, l) = struct.unpack('>HH', rest[:4])
				v = rest[4:4 + l]
				setattr(self, 'opt_0x%04x' % t, v)
				rest = rest[4 + l:]
		elif self.command_id == SMPP_SUBMIT_SM_RESP or self.command_id == SMPP_DELIVER_SM_RESP:
			self.obj_type = None
			self.message_id, rest = self.body.split('\x00', 1)
		elif self.command_id in [SMPP_BIND_RX_RESP, SMPP_BIND_TX_RESP, SMPP_BIND_TRX_RESP]:
			self.obj_type = None
			self.system_id, rest = self.body.split('\x00', 1)
		elif self.command_id in [SMPP_BIND_RX, SMPP_BIND_TX, SMPP_BIND_TRX]:
			self.obj_type = 'SMPP_BIND_TX'
			self.user, rest = self.body.split('\x00', 1)
			self.password, rest = rest.split('\x00', 1)
			self.system_type, rest = rest.split('\x00', 1)
			(self.interface_version, self.addr_ton, self.dest_addr_npi) = struct.unpack('>BBB', rest[:3])
			self.addr_range, rest = rest[3:].split('\x00', 1)
		elif self.command_id == SMPP_DATA_SM:
			self.service_type, rest = self.body.split('\x00', 1)
			(self.source_addr_ton, self.source_addr_npi) = struct.unpack('>BB', rest[:2])
			self.source_addr, rest = rest[2:].split('\x00', 1)
			(self.dest_addr_ton, self.dest_addr_npi) = struct.unpack('>BB', rest[:2])
			self.dest_addr, rest = rest[2:].split('\x00', 1)
			(self.esm_class, self.registered_delivery, self.data_coding) = struct.unpack('>BBB', rest[:3])
			self.obj_type = 'SMPP_DATA_SM'
			# optional parameters
			rest = rest[3:]
			while len(rest) > 4:
				(t, l) = struct.unpack('>HH', rest[:4])
				v = rest[4:4 + l]
				setattr(self, 'opt_0x%04x' % t, v)
				rest = rest[4 + l:]
		return None

	def response(self, status=0x00):
		if self.command_id & 0x80000000 == 0x80000000:
			return None
		p = None
		if self.command_id == SMPP_SUBMIT_SM or self.command_id == SMPP_DELIVER_SM:
			p = SUBMIT_SM_RESP()
		elif self.command_id in [SMPP_BIND_RX, SMPP_BIND_TX, SMPP_BIND_TRX]:
			p = BIND_SM_RESP()
		else:
			p = PDU()
		
		p.command_id = self.command_id | 0x80000000
		p.sequence_number = self.sequence_number
		p.command_status = status
		return p

	def getcmdlen(self):
		if self.body:
			length = len(self.body)
		else:
			length = 0
		self.command_length = length + 16
		return self.command_length

	def build(self):
		pass

	def __str__(self):
		self.build()
		self.getcmdlen()
		b = self.body
		if not b:
			return struct.pack('>4I',int(self.command_length & 0xFFFFFFFF), self.command_id,
				int(self.command_status & 0xFFFFFFFF), int(self.sequence_number & 0xFFFFFFFF))
		l = len(b)
		return struct.pack('>4I'+`l`+'s', self.command_length , self.command_id,
			self.command_status, self.sequence_number, b)

	def dump(self, label=''):
		if self._changed == False and self._look is not None:
			return self._look
		self.build()
		self.getcmdlen()
		s = label + ':\n'
		s += "CommandLength = 0x%08x (%d)\n" % (self.command_length,self.command_length)
		s += "CommandID = 0x%08x (%s)\n" % (self.command_id, SMPP_cmd_name(self.command_id))
		s += "CommandStatus = 0x%08x\n" % self.command_status
		s += "SequenceNumber = 0x%08x\n" % self.sequence_number	

		schema = config.get_schema(self.obj_type)
		if schema is None:
			return s
		o = ''
		for x in dir(self):
			if 'props' in schema:
				if x in schema['props']:
					if 'tag' in schema['props'][x]:
						t = schema['props'][x]['type'] 
						if t == 'int8':
							o += "%s: 0x%02x\n" % (x, getattr(self, x))
						elif t == 'int16':
							o += "%s: 0x%04x\n" % (x, getattr(self, x))
						else:
							o += "%s: %s\n" % (x, repr(eval('self.' + x )))
					else:
						s += "%s: %s\n" % (x, repr(eval('self.' + x )))
				elif x[:4] == 'opt_':
					tag = int(x[4:], 16)
					for p in schema['props']:
						if 'tag' in schema['props'][p] and schema['props'][p]['tag'] == tag:
							t = schema['props'][p]['type']
							if t == 'int8':
								(b, ) = struct.unpack('B', getattr(self, x))
								o += "%s: 0x%02x\n" % (p, b)
							elif t == 'int16':
								(h, ) = struct.unpack('<H', getattr(self, x))
								o += "%s: 0x%04x\n" % (p, h)
							else:
								o += "%s: %s\n" % (p, repr(eval('self.' + x)))
							break
					else:						
						o += "%s: %s\n" % (x, repr(eval('self.' + x)))
		s += o
		self._look = s
		return s

	def build_optional_parameters(self):
		s = ''
		props = config.get_schema(self.obj_type)['props']
		for x in dir(self):
			if x in props and 'tag' in props[x]:
				val = getattr(self, x)
				t = props[x]['type']
				s += struct.pack('>H', props[x]['tag'])
				if t == 'int8':
					s += struct.pack('>H', 0x0001)
					s += struct.pack('B', val)
				elif t == 'int16':
					s += struct.pack('>H', 0x0002)
					s += struct.pack('H', val)
				elif t == 'str':
					l = len(val)
					s += struct.pack('>H', l)
					s += struct.pack(`l`+'s', val)
		self.body += s

class SUBMIT_SM_RESP(PDU):
	""" Needed for answer back message id """
	message_id = ''
	def build(self):
		self.body = struct.pack(`len(self.message_id)`+'s', self.message_id) + '\0'

class BIND_SM_RESP(PDU):
	""" Needed for answer back system id """
	def __init__(self):
		self.system_id = None
		PDU.__init__(self)

	def build(self):
		if self.system_id:
			self.body = struct.pack(`len(self.system_id)`+'s', self.system_id) + '\0'

class SUBMIT_SM(PDU):
	command_id = SMPP_SUBMIT_SM

	def build(self):
		if self._changed == False and self.body is not None:
			return
		l = len(self.service_type)
		s = struct.pack(`l`+'s', self.service_type) + '\0'
		s += struct.pack('B', self.source_addr_ton)
		s += struct.pack('B', self.source_addr_npi)
		l = len(self.source_addr)
		s += struct.pack(`l`+'s', self.source_addr) + '\0'
		s += struct.pack('B', self.dest_addr_ton)
		s += struct.pack('B', self.dest_addr_npi)
		l = len(self.dest_addr)
		s += struct.pack(`l`+'s', self.dest_addr) + '\0'
		s += struct.pack('B', self.esm_class)
		s += struct.pack('B', self.protocol_id)
		s += struct.pack('B', self.priority_flag)
		l = len(self.schedule_delivery_time)
		s += struct.pack(`l`+'s', self.schedule_delivery_time) + '\0'
		l = len(self.validity_period)
		s += struct.pack(`l`+'s', self.validity_period) + '\0'
		s += struct.pack('B', self.registered_delivery)
		s += struct.pack('B', self.replace_if_present_flag)
		s += struct.pack('B', self.data_coding)
		s += struct.pack('B', self.sm_default_msg_id)
		self.sm_length = len(self.short_message)
		s += struct.pack('B', self.sm_length)
		s += struct.pack(`self.sm_length`+'s', self.short_message)
		self.body = s
		PDU.build_optional_parameters(self)

class DELIVER_SM(SUBMIT_SM):
	def __init__(self):
		SUBMIT_SM.__init__(self)
		self.command_id = SMPP_DELIVER_SM

class DATA_SM(SUBMIT_SM):
	def __init__(self):
		SUBMIT_SM.__init__(self)
		self.command_id = SMPP_DATA_SM

class BIND(PDU):
	def build(self):
		l = len(self.user)
		s = struct.pack(`l`+'s',self.user) + '\0'
		l = len(self.password)
		s += struct.pack(`l`+'s',self.password) + '\0'
		l = len(self.system_type)
		s += struct.pack(`l`+'s', self.system_type) + '\0'
		s += struct.pack('B', self.smpp_version)
		s += struct.pack('B', self.addr_ton)
		s += struct.pack('B', self.addr_npi)
		l = len(self.addr_range)
		s += struct.pack(`l`+'s', self.addr_range) + '\0'
		self.body = s

class BIND_RX(BIND):
	command_id = SMPP_BIND_RX

class BIND_TX(BIND):
	command_id = SMPP_BIND_TX

class BIND_TRX(BIND):
	command_id = SMPP_BIND_TRX

class UNBIND(PDU):
	command_id = SMPP_UNBIND

from gevent import sleep
from utils import GServer, GClient, config, GAnswer
from gevent.event import AsyncResult, Timeout

class SmppServer(GServer):
	res = {}
	def send(self, where, what):
		p = config.get_obj(what)
		if p is None:
			return GAnswer(False, 'No such object: %s' % what)
		self.seq += 1
		p.sequence_number = self.seq
		self.log(p.dump('sending'))
		ar = AsyncResult()
		self.res[self.seq] = ar
		a = GServer.send(self, where, p)
		if a.ok:
			r = None
			try:
				r = ar.get(timeout = self.timeout)
			except Timeout:
				s = 'timeout %d sec for seq:0x%08x' % (self.timeout, p.sequence_number)
				self.log_error(s)
				a.load = s
				return a
			a.load = r.dump('get')
		return a

	def handler(self, socket, address):
		try:
			hdr = socket.recv(16)
		except Exception, e:
			self.log_error(e)
			hdr = ''
		if len(hdr) == 16:
			p = PDU(hdr)
			if p.command_length > 16:
				rest = socket.recv(p.command_length - 16)
				p = PDU(hdr + rest)
				self.log(p.dump('get'))
				resp = p.response()
				resp.system_id = 'Gustaw SMSC'
				socket.send(str(resp))
				self.log(resp.dump('send'))
		else:
			self.log('Connection dropped %s:%s' % address)
			return

		# auth ok - main loop
		while True:
			try:
				hdr = socket.recv(16)
			except Exception, e:
				self.log_error(e)
				break
			if len(hdr) == 16:
				p = PDU(hdr)
				if p.command_length > 16:
					try:
						rest = socket.recv(p.command_length - 16)
					except Exception, e:
						self.log_error(e)
						break
					p = PDU(hdr + rest)
				resp = p.response()
				if p.sequence_number in self.res:
					# does not log this beacause it will be probably logged later
					self.res[p.sequence_number].set(p)
				else:
					self.loga(GAnswer(load = p.dump('get'), op = p.command_id))
				if resp == None:
					continue
				self.pool.spawn(self.send_resp, socket, resp, p)
			else:
				break

	def send_resp(self, socket, p, req):
		if p.command_id == SMPP_SUBMIT_SM_RESP:
			if self.submit_sm_resp_delay > 0.0:
				sleep(self.submit_sm_resp_delay)
			if self.on_submit_sm != '':
				a = config.get_obj(self.on_submit_sm)
				if a is not None:
					self.pool.spawn(a.run)
			p.command_status = self.submit_sm_resp_code

		self.log(p.dump('sending'))
		try:
			socket.send(str(p))
		except Exception, e:
			self.log_error(e)

	def send_socket(self, socket, p):
		return GServer.send_socket(self, socket, str(p))

class SmppClient(GClient):
	res = {}

	def send(self, sms, wait = True):
		if not self.connected:
			return GAnswer(False, load = 'not connected')
		p = config.get_obj(sms)
		if p is None:
			return GAnswer(False, load = 'no such object %s' % sms)
		else:
			p.sequence_number = self.seq
			self.seq += 1
			if wait:
				ar = AsyncResult()
				self.res[p.sequence_number] = ar
			try:
				self.socket.send(str(p))
			except Exception, e:
				a = GAnswer(False, load = str(e)) 
				if wait:
					self.res.pop(p.sequence_number)
				return a
			self.log(p.dump('sent'))

			if wait:
				try:
					r = ar.get(timeout = self.timeout)
				except Timeout:
					return GAnswer(False, load = 'timeout %d sec for seq:0x%08x' % (self.timeout, p.sequence_number))
				a = GAnswer()
				a.load = r.dump('get')
				return a

	def _recv(self):
		hdr = self.socket.recv(16)
		if len(hdr) == 16:
			p = PDU(hdr)
		else:
			return None
		if p.command_length > 16:
			rest = self.socket.recv(p.command_length - 16)
			p = PDU(hdr + rest)

		if p.command_id & 0x80000000 == 0x80000000 and p.sequence_number in self.res:
			self.res[p.sequence_number].set(p)
		else:
			self.log(p.dump('get'))
		return p

	def get_msg(self, p):
		resp = p.response()
		if resp is not None:
			self.log(resp.dump('send'))
			try:
				self.socket.send(str(resp))
			except Exception, e:
				self.connected = False
				self.log_error(e)

